英文地址: Sharing Location using Inherited Widget
某些时候,你需要在你的应用程序中多个路由或者控件中使用一个位置信息,在所有需要的地方配置和管理flutter的location
插件。相反的是,我们可使用Inherited Widget
控件来在多个控件之间共享数据。
从命令行敲入如下代码flutter create -t package location_context
-t参数是选择模版,-t package意思是快速创建一个模块包的模版样本代码。
第一步:导入依赖
在pubspec.yaml
文件的dependences:
1 | location: ^1.3.4 |
这个location
插件用于与GPS硬件进行交互,来获取设备真实的位置信息用于共享数据。
这个quiver
提供的hashObjects便捷方法,将用于创建Position
类的hashCode。备注:flutter内部也使用这个包,所以我们应该确保版本同步,如果依赖发生冲突,将会产生一个警告。
当我们在pubspec.yaml
文件中添加这些依赖后,运行flutter packages get
来下载安装获取这些依赖包。
第二步: 创建代码库
打开lib/location_context.dart
,这个是我们包的入口,除了library
第一行行外,替换掉生成默认的样本代码。
1 | import 'dart:async'; |
该文件是我们包的入口,并且使用part
来导入我们其他的文件,我们所有的导入都放到这个文件里面,即使我们不在这个文件中使用,这将会part
的文件中使用。
使用part
可以允许我们将一个库分割成多个dart文件。因为它们全部都是是该库的一部分(同一个库),私有的变量也可以在所有指定的文件中访问到,提到这里是因为Position
有个私有的构造函数,希望在LocationContext
中也能使用到。
第三步: 创建Position类
创建lib/src/position.dart
,用来定义Position
类,用于装载携带我们的位置数据。location
插件将会返回返回一个Map对象。之后我们将会使用这个对象来初始化我们的Position
实例。
1 | part of location_context; // 与part 是成对出现的,表示在该库内 |
所有的这些属性都是location
插件会返回的数据项,我们也定义了一个构造函数来让我们能够手动地定义每个属性。
因为所有的属性变量是final
,运行时不可变,我也将会使用Quiver的hashObjects
方法来计算我们的_hashCode,这样不需要每次需要的时候来计算它,因为它不会变。
此外,我们还需要能够与其他对象进行比较,以及能够优雅地以字符串的形式输出这些对象用于调试等等,在_fromMap构造函数下添加如下代码:
1 | bool operator ==(dynamic other) { |
首先我们重写了我们的==
运算符,来做一个比较的运算,通过比较this
和other
的hashCode
来判断它们包含的值是否一致,如果hashCode的值一致,那么它们将包含相同的数据。
创建Location Context
创建lib/src/context.dart
,该文件将会包含Inherited Widget
来实现我们的魔法。当我们创建一个控件的时候,我需要考虑一下几点:
- 为了单元测试,我们需要能够从该包中
mock
这个Location
对象。 - 我们需要实际创建这个继承的控件,来共享我们的存储的数据。
- 我们能够更新以及存储这些共享的数据值。
我们在第二步中,在
Location 依赖注入
1 | part of location_context; |
首先,我们使用typedef
为我们的工厂函数进行了类型定义,我将用该工厂函数来进行依赖注入,方法代码仅仅是不接受任务参数来返回Location
实例。
其次,我定义了一个方法能够覆盖我们的工厂方法,以至于能够注入不同的依赖。mockLocation
接收一个回调函数作为依赖,然后将该回调赋值给默认的工厂函数。
最后,定义我们的默认的工厂函数来返回我们Location
依赖的实例对象。在测试的时候我们可以使用mockLocation
方法来允许我们的控件能够获取到一个mocked的版本。
我们使用@visibleForTesting
标记这两部分进行模拟,来让开发者知道这些构造是“私有”,在他们的代码中不应该访问的,即使在这个包外是可见的。
这个Inherited Widget
继承控件本质非常简单,我们传给它一些信息,然后这些信息会在控件树层次中高效地向下传播,它控件下的子控件能够访问到这些信息,达到信息共享的效果,我们将共享三个值:lastLocation
,currentLocation
,和error
。
创建Inerited Widget
lastLocation
和currentLocation
这个两个变量不言自明,使我们的应用程序能够进行方向向量上的计算。error仅仅是一个字符串字段,来包含我们可能遇到的异常错误信息,该控件的父类是ProxyWidget
代理控件,该控件不会去构建一个新的控件,而是使用提供给它的子控件,也就是child
属性。
下面仅仅是这个类的开始部分,我们将在随后的部分进行修改调整。
1 | class LocationContext extends InheritedWidget { // 继承InheritedWidget |
我们的命名构造函数以_为前缀,这意味着它只能在本身所在的包中进行初始化,这对继承控件来说十分重要的。
of
方法正是发挥魔力的地方,该方法允许任意的子控件能够调用LocationConetext.of(context)
,然后返回该控件的实例,允许它们能够访问到该控件的公共成员。
最后,updateShouldNotify
用于是否通知控件树层次结构中该控件节点以下的控件已经发生更新,以至于它们能够重新构建,如果必要 ,如果其属性发生变化,这个控件将会通知一个更新。
创建Stateful Widget
我们的状态控件将会与Location
进行交互,存储并更新我们的位置信息,并将其传入我们创建的继承控件中。
添加下面代码到lib/src/conetext.dart
底部:
1 | class _LocationContextWrapper extends StatefulWidget { |
我们有一个StatefulWidget
控件且包含了一个名为child
的成员控件,这个child控件将会被
LocationContext`包裹,接收位置更新。
你将会注意到我们的三个成员变量和我们LocationContext
存储的三个变量很相似,这个因为这三个_error
,_currentLocation
,_lastLocation
值正是我们将要本地管理并传入LocationContext
的值。
最后,_locationChangedSubsription
将用于从我们的Location
对象来监听位置更新,注意这个将会返回一个Map
对象,需要将其转化为Map
实例。
接下来初始化我们的状态,在_locationChangedSubscription
下添加如下代码:
1 |
|
我们在这个方法中,做了两个事情:1.我们订阅_location.onLocationChanged
流来监听位置更新,每当接收到一个更新,我们将会从Map
中创建一个Position
实例,然后从接收的位置信息中通过清空_error
,更新_lastLocation
,设置_currentLocation
来更新我们的state,2.初始化位置信息,来载入初始的位置数据,在这个方法我们使用async/await
语法糖来使得我们的代码看起来更加线性,但是我们也可以仅仅使用Future
,添加如下方法:
1 | void initLocation() async { |
我们await
当前返回设备位置信息,然后像我们之前的那样更新我们的状态,因为这个是我们第一次时候的状态,我将要设置_lastLocation
和_currentLoaction
相同的值,如果发生异常,我们捕获该异常,设置_error
。
接下来,我们要确保正确地取消订阅流,这将会在dispose
中处理,该方法会在控件销毁的时候被调用。
1 | @override |
这里使用?.
确保null值安全,这个要小心,如果没有初始化这个订阅,这个可能会发生错误。所以做一个空值判断。
最后,我们将会利用这些信息来实际构建,下面这个地方是我们实际使用Inherited Widget
这个代理控件的地方:
1 |
|
简单地说,我们只是构建了Inherited Widget
,然后将在State
中收集到的数据传给它,同时Inherited Widget
包裹child
,Inherited Widget
控件为代理控件,本身不会构建子控件,相反会将child
成员控件作为其子控件。这就意味着无论何时我们获取一个新的位置以及更新我们的状态,就会传入新的信息给LocationContext
,进而子节点的控件能够访问到这些数据。
此外,我们缺少了些重要的东西,一个使用_LocationContextWrapper
的方法,因为它是包内私有的,我们需要添加一个方法来帮助我们使用它,我添加一个公有的静态方法到LocationContext
中。
1 | static Widget around(Widget child, {Key key}) { |